#! /bin/bash

# File:   generic.sh
# Author: Sayan Chaliha
# Email:  sayan@yahoo-inc.com
# Date:   November 16, 2011
# Breif:  Contains generic functions used in other scripts

declare -ir LOG_LEVEL_TRACE=0;
declare -ir LOG_LEVEL_DEBUG=1;
declare -ir LOG_LEVEL_INFO=2;
declare -ir LOG_LEVEL_WARN=3;
declare -ir LOG_LEVEL_ERROR=4;

declare -ir LOG_LEVEL_DEFAULT=${LOG_LEVEL_INFO};
declare -r  LOG_FILE_DEFAULT="/dev/stderr";

declare -i  LOG_LEVEL=${LOG_LEVEL:-${LOG_LEVEL_DEFAULT}};
declare -r  LOG_FILE=${LOG_FILE:-${LOG_FILE_DEFAULT}};

# Logs an error to the specified log.
#
# Parameters:
#   $@ -- Error message
#
# Returns:
#   0 always
function logError () {
  local MESSAGE="${*}";
  if [ ${LOG_LEVEL} -le ${LOG_LEVEL_ERROR} ];
  then                                                                        \
    CALLER="$(caller)";                                                       \
    LINE=$(echo ${CALLER} | awk '{ print $1; }');                             \
    FILE=$(basename $(echo ${CALLER} | awk '{ print $2; }'));                 \
    MESSAGE=$(printf "[ERROR]\t[%s:%d]\t%s" "${FILE}" ${LINE} "${MESSAGE}");  \
    printf "%s\n" $"${MESSAGE}" >>${LOG_FILE};                                \
  fi;
  return 0;
}

# Logs a warning to the specified log.
#
# Parameters:
#   $@ -- Warning message
#
# Returns:
#   0 always
function logWarn () {
  local MESSAGE="${*}";
  if [ ${LOG_LEVEL} -le ${LOG_LEVEL_WARN} ];
  then                                                                        \
    CALLER="$(caller)";                                                       \
    LINE=$(echo ${CALLER} | awk '{ print $1; }');                             \
    FILE=$(basename $(echo ${CALLER} | awk '{ print $2; }'));                 \
    MESSAGE=$(printf "[WARN]\t[%s:%d]\t%s" "${FILE}" ${LINE} "${MESSAGE}");   \
    printf "%s\n" $"${MESSAGE}" >>${LOG_FILE};                                \
  fi;
  return 0;
}

# Logs an info message to the specified log.
#
# Parameters:
#   $@ -- Info message to log
#
# Returns:
#   0 always
function logInfo () {
  local MESSAGE="${*}";
  if [ ${LOG_LEVEL} -le ${LOG_LEVEL_INFO} ];
  then                                                                        \
    CALLER="$(caller)";                                                       \
    LINE=$(echo ${CALLER} | awk '{ print $1; }');                             \
    FILE=$(basename $(echo ${CALLER} | awk '{ print $2; }'));                 \
    MESSAGE=$(printf "[INFO]\t[%s:%d]\t%s" "${FILE}" ${LINE} "${MESSAGE}");   \
    printf "%s\n" $"${MESSAGE}" >>${LOG_FILE};                                \
  fi;
  return 0;
}

# Logs a debug message to the specified log.
#
# Parameters:
#   $@ -- Debug message to log
#
# Returns:
#   0 always
function logDebug () {
  local MESSAGE="${*}";
  if [ ${LOG_LEVEL} -le ${LOG_LEVEL_DEBUG} ];
  then                                                                        \
    CALLER="$(caller)";                                                       \
    LINE=$(echo ${CALLER} | awk '{ print $1; }');                             \
    FILE=$(basename $(echo ${CALLER} | awk '{ print $2; }'));                 \
    MESSAGE=$(printf "[DEBUG]\t[%s:%d]\t%s" "${FILE}" ${LINE} "${MESSAGE}");  \
    printf "%s\n" $"${MESSAGE}" >>${LOG_FILE};                                \
  fi;
  return 0;
}

# Logs a trace message to the specified log.
#
# Parameters:
#   $@ -- Trace message to log
#
# Returns:
#   0 always
function logTrace () {
  local MESSAGE="${*}";
  if [ ${LOG_LEVEL} -le ${LOG_LEVEL_TRACE} ];
  then                                                                        \
    CALLER="$(caller)";                                                       \
    LINE=$(echo ${CALLER} | awk '{ print $1; }');                             \
    FILE=$(basename $(echo ${CALLER} | awk '{ print $2; }'));                 \
    MESSAGE=$(printf "[TRACE]\t[%s:%d]\t%s" "${FILE}" ${LINE} "${MESSAGE}");  \
    printf "%s\n" $"${MESSAGE}" >>${LOG_FILE};                                \
  fi;
  return 0;
}

# Checks the return code of the last command executed. Optionally, a message can
# be printed to the error console and can also abort script execution and return
# with an error.
#
# Parameters:
#   $1 -- Message to print to the console in case of error        [optional]
#   $2 -- Boolean; 1 (or "true") indicates fatal error            [optional]
#
# Returns:
#   0 if no error was detected; 1 otherwise
function checkReturnCode () {
  local RETURN_CODE="${?}";

  logTrace "ENTER ${FUNCNAME}";
  local MESSAGE="${1}";
  local IS_FATAL="${2}";
  local DO_EXIT=$([[ "${IS_FATAL}" == [tT][rR][uU][eE] ]]; echo ${?});
  local DO_EXIT=$([ "${IS_FATAL}" = "1" -o "${DO_EXIT}" = "0" ]; echo ${?});

  if [ ${RETURN_CODE} -ne 0 ];
  then                                                                \
    if [ -n "${MESSAGE}" ];                                           \
    then                                                              \
      logError "${MESSAGE}";                                          \
    fi;                                                               \
    if [ ${DO_EXIT} -eq 0 ];                                          \
    then                                                              \
      logError "Abnormal Termination requested by: $(basename $(echo  \
          $(caller) | awk '{ print $2; }'):$(echo $(caller) | awk     \
          '{ print $1; }'))";                                         \
      logTrace "EXIT  ${FUNCNAME}";                                   \
      exit ${RETURN_CODE};                                            \
    fi;                                                               \
    return 1;                                                         \
  fi;
  logTrace "EXIT  ${FUNCNAME}";
  return 0;
}

# Changes the current directory to the directory containing the shell script
# which invoked this function. Usually doesn't fail, but if it does, it will
# cause the script to stop execution and exit with an error. If the script which
# invokes this function already resides in the current working directory, then
# calling this function has no effect.
#
# Returns:
#   0 always
function setCwdToMyDir () {
  logTrace "ENTER ${FUNCNAME}";
  local MY_NAME="$(basename ${0})";
  local MY_DIR="$(dirname ${0})";


  if ! [ -e "${PWD}/${MY_NAME}" ];
  then                                                              \
    if ! [[ ${MY_DIR} =~ / ]];                                      \
    then                                                            \
      MY_DIR="${PWD}/${MY_DIR}";                                    \
    fi;                                                             \
    pushd "${MY_DIR}" 1>/dev/null 2>&1;                             \
    checkReturnCode "Unable to change working directory" true;      \
    [ -e "${PWD}/${MY_NAME}" ];                                     \
    checkReturnCode "Unkown error: ${MY_NAME} not in ${PWD}" true;  \
  fi;
  logTrace "EXIT  ${FUNCNAME}";
  return 0;
}

# Executes a command and displays timing information. Mostly useful for
# debugging and performance analysis. The timing information is 'echo'ed to the
# standard output so you can capture it in a variable from there if required.
#
# Parameters:
#   $1 -- Command to execute
#
# Returns:
#   0 on success; return code of command execution on failure
function timedExecute () {
  logTrace "ENTER ${FUNCNAME}";
  declare -i code=0;

  if ! [ "${1}" -o -n "${1}" ];
  then                                              \
    logError "Invalid arguments";                   \
    code=1;                                         \
  else                                              \
    time -p ${*};                                   \
    checkReturnCode "Failed to execute \"${*}\"";   \
    code=${?};                                      \
  fi;
  logTrace "EXIT  ${FUNCNAME}";
  return ${code};
}

# Prompts the user to select from one option from a list of options (specified
# as arguments to the function). If the user selects a valid option, it's value
# is 'echo'ed to the standard output and can be caught in a variable like so:
#   option=$(promptOptions hello world);
# In the above example if the user doesn't enter a valid option, then 'option'
# is set to the NULL string.
#
# Parameters:
#   $@ -- Space-separated list of options
#
# Returns:
#   1 if no valid arguments are supplied; 0 otherwise
function promptOptions () {
  logTrace "ENTER ${FUNCNAME}";
  local OPTIONS="${@}";
  declare -i code=0;

  if ! [ "${OPTIONS}" -o -n "${OPTIONS}" ];
  then                                                \
    logError "Invalid arguments";                     \
    code=1;                                           \
  else                                                \
    select option in ${OPTIONS};                      \
    do                                                \
      if ! [ "${option}" ];                           \
      then                                            \
        logError "Invalid choice selected: ${REPLY}"; \
        code=1;                                       \
      else                                            \
        echo "${option}";                             \
      fi;                                             \
      break;                                          \
    done;                                             \
  fi;                                                 \
  logTrace "EXIT  ${FUNCNAME}";
  return ${code};
}

# Substitutes a pattern in specified file with given replacement string or blank
# if no replacement is specified.
#
# Parameters:
#   $1 -- Path to file
#   $2 -- Search pattern; maybe a regular expression
#   $3 -- Replacement                                     [optional]
function replaceInFile () {
  logTrace "ENTER ${FUNCNAME}";
  local FILE="${1}";
  local SEARCH="${2}";
  local REPLACEMENT="${3:-""}";
  declare -i code=0;

  if [ ! "${*}" -o -z "${1}" -o -z "${2}" -o ${#@} -lt 2 ];
  then                                                                      \
    logError "Invalid arguments";                                           \
    code=1;                                                                 \
  else                                                                      \
    if ! [ -e "${FILE}" ];                                                  \
    then                                                                    \
      logError "File not found: \"${FILE}\"";                               \
      code=1;                                                               \
    else                                                                    \
      sed -E -i ".bak" -e 's/'"${SEARCH}"'/'"${REPLACEMENT}"'/' "${FILE}";  \
      checkReturnCode "Pattern replacement in ${FILE} failed";              \
      code=${?};                                                            \
    fi;                                                                     \
  fi;
  logTrace "EXIT  ${FUNCNAME}";
  return ${code};
}

# Reads a property from a config file. The config file may contain key-value
# pairs of the form 'key = value' (similar to Ant property files). If duplicate
# keys are found, the function will echo the value of all matched keys separated
# by newline. Returns a null string if property is not found.
#
# Parameters:
#   $1 -- Path to config file
#   $2 -- Property key
#
# Returns:
#   0 on success; 1 on error
function readConfigProperty () {
  logTrace "ENTER ${FUNCNAME}";
  local FILE="${1}";
  local PROP="${2}";
  declare -i code=0;

  if [ ! "${*}" -o -z "${1}" -o -z "${2}" ];
  then                                                                        \
    logError "Invalid arguments";                                             \
    code=1;                                                                   \
  else                                                                        \
    if ! [ -e "${FILE}" ];                                                    \
    then                                                                      \
      logError "File not found: \"${FILE}\"";                                 \
      code=1;                                                                 \
    else                                                                      \
      sed -E -e 's/#.*$//' <"${FILE}" >".tmp.read.config";                    \
      awk "BEGIN { FS=\"=\"; } \$0 !~ /^(#)+/ && \$0 ~ /(${PROP})[ \n\t]*\=/  \
          { gsub(/^ /, \"\", \$2); gsub(/ $/, \"\", \$2); print \$2; }"       \
          ".tmp.read.config";                                                 \
      checkReturnCode "Unable to read property \"${PROP}\"";                  \
      code=${?};                                                              \
      rm -f ".tmp.read.config";                                               \
    fi;                                                                       \
  fi;
  logTrace "EXIT  ${FUNCNAME}";
  return ${code};
}

# Writes a property to a config file. The config file will contain key-value
# pairs of the form 'key = value' (similar to Ant property files). If duplicate
# keys are found, the function will REMOVE ALL PREVIOUS occurrences of the key.
#
# Parameters:
#   $1 -- Path to config file
#   $2 -- Property key
#   $3 -- Property value
#   $4 -- Textual description of the property       [optional]
function writeConfigProperty () {
  logTrace "ENTER ${FUNCNAME}";
  local FILE="${1}";
  local KEY="${2}";
  local VALUE="${3}";
  local DESC="${4}";
  declare -i code=0;

  if [ ! "${*}" -o -z "${1}" -o -z "${2}" -o -z "${3}" ]; then
    logError "Invalid arguments";                                     \
    code=1;                                                           \
  else                                                                \
    if [ -e "${FILE}" ];                                              \
    then                                                              \
      sed -E -i ".bak" -e 's/^[ \r\n\t]*'"${KEY}"'[ \r\n\t]*\=.*$//'  \
          -e '/^[ \r\n\t]*$/d' "${FILE}";                             \
    fi;                                                               \
    printf "%s = %s" "${KEY}" "${VALUE}" >>"${FILE}";                 \
    if [ -n "${DESC}" ]; then                                         \
      printf " # %s\n" "${DESC}" >> "${FILE}";                        \
    fi;                                                               \
  fi;

  logTrace "EXIT  ${FUNCNAME}";
}

#
#
#
function assertFileExists () {
  logTrace "ENTER ${FUNCNAME}";
  local FILE="${1}";

  if ! [ "${FILE}" -o -e "${FILE}" ];
  then                                                  \
    logError "Assertion failed: \"${FILE}\" not found"; \
    exit 1;                                             \
  fi;
  logTrace "EXIT  ${FUNCNAME}";
}

#
#
#
function assertFilesExist () {
  logTrace "ENTER ${FUNCNAME}";
  for file in "${*}";
  do                          \
    assertFileExists ${file}; \
  done;
  logTrace "EXIT  ${FUNCNAME}";
}

logDebug "$(basename $(echo $(caller) | awk '{ print $2; }'):$(echo $(caller) \
    | awk '{ print $1; }')) included generic.sh";
